package model

import (
	"errors"
	"fmt"
	"io"
	"strings"

	"gitee.com/coodder/unipdf/common"
	"gitee.com/coodder/unipdf/core"
	"gitee.com/coodder/unipdf/core/security"
)

// PdfReader represents a PDF file reader. It is a frontend to the lower level parsing mechanism and provides
// a higher level access to work with PDF structure and information, such as the page structure etc.
type PdfReader struct {
	parser         *core.PdfParser
	root           core.PdfObject
	pagesContainer *core.PdfIndirectObject
	pages          *core.PdfObjectDictionary
	pageList       []*core.PdfIndirectObject
	PageList       []*PdfPage
	pageCount      int
	catalog        *core.PdfObjectDictionary
	outlineTree    *PdfOutlineTreeNode
	AcroForm       *PdfAcroForm

	modelManager *modelManager

	// Lazy loading: When enabled reference objects need to be resolved (via lookup, disk access) rather
	// than loading entire document into memory on load.
	isLazy bool

	// For tracking traversal (cache).
	traversed map[core.PdfObject]struct{}
	rs        io.ReadSeeker
}

// NewPdfReader returns a new PdfReader for an input io.ReadSeeker interface. Can be used to read PDF from
// memory or file. Immediately loads and traverses the PDF structure including pages and page contents (if
// not encrypted). Loads entire document structure into memory.
// Alternatively a lazy-loading reader can be created with NewPdfReaderLazy which loads only references,
// and references are loaded from disk into memory on an as-needed basis.
func NewPdfReader(rs io.ReadSeeker) (*PdfReader, error) {
	pdfReader := &PdfReader{
		rs:           rs,
		traversed:    map[core.PdfObject]struct{}{},
		modelManager: newModelManager(),
		isLazy:       false,
	}

	// Create the parser, loads the cross reference table and trailer.
	parser, err := core.NewParser(rs)
	if err != nil {
		return nil, err
	}
	pdfReader.parser = parser

	isEncrypted, err := pdfReader.IsEncrypted()
	if err != nil {
		return nil, err
	}

	// Load pdf doc structure if not encrypted.
	if !isEncrypted {
		err = pdfReader.loadStructure()
		if err != nil {
			return nil, err
		}
	}

	return pdfReader, nil
}

// NewPdfReaderLazy creates a new PdfReader for `rs` in lazy-loading mode. The difference
// from NewPdfReader is that in lazy-loading mode, objects are only loaded into memory when needed
// rather than entire structure being loaded into memory on reader creation.
// Note that it may make sense to use the lazy-load reader when processing only parts of files,
// rather than loading entire file into memory. Example: splitting a few pages from a large PDF file.
func NewPdfReaderLazy(rs io.ReadSeeker) (*PdfReader, error) {
	pdfReader := &PdfReader{
		rs:           rs,
		traversed:    map[core.PdfObject]struct{}{},
		modelManager: newModelManager(),
		isLazy:       true,
	}

	// Create the parser, loads the cross reference table and trailer.
	parser, err := core.NewParser(rs)
	if err != nil {
		return nil, err
	}
	pdfReader.parser = parser

	isEncrypted, err := pdfReader.IsEncrypted()
	if err != nil {
		return nil, err
	}

	// Load pdf doc structure if not encrypted.
	if !isEncrypted {
		err = pdfReader.loadStructure()
		if err != nil {
			return nil, err
		}
	}

	return pdfReader, nil
}

// PdfVersion returns version of the PDF file.
func (r *PdfReader) PdfVersion() core.Version {
	return r.parser.PdfVersion()
}

// IsEncrypted returns true if the PDF file is encrypted.
func (r *PdfReader) IsEncrypted() (bool, error) {
	return r.parser.IsEncrypted()
}

// GetEncryptionMethod returns a descriptive information string about the encryption method used.
func (r *PdfReader) GetEncryptionMethod() string {
	crypter := r.parser.GetCrypter()
	return crypter.String()
}

// Decrypt decrypts the PDF file with a specified password.  Also tries to
// decrypt with an empty password.  Returns true if successful,
// false otherwise.
func (r *PdfReader) Decrypt(password []byte) (bool, error) {
	success, err := r.parser.Decrypt(password)
	if err != nil {
		return false, err
	}
	if !success {
		return false, nil
	}

	err = r.loadStructure()
	if err != nil {
		common.Log.Debug("ERROR: Fail to load structure (%s)", err)
		return false, err
	}

	return true, nil
}

// CheckAccessRights checks access rights and permissions for a specified password.  If either user/owner
// password is specified,  full rights are granted, otherwise the access rights are specified by the
// Permissions flag.
//
// The bool flag indicates that the user can access and view the file.
// The AccessPermissions shows what access the user has for editing etc.
// An error is returned if there was a problem performing the authentication.
func (r *PdfReader) CheckAccessRights(password []byte) (bool, security.Permissions, error) {
	return r.parser.CheckAccessRights(password)
}

// Loads the structure of the pdf file: pages, outlines, etc.
func (r *PdfReader) loadStructure() error {
	if r.parser.GetCrypter() != nil && !r.parser.IsAuthenticated() {
		return fmt.Errorf("file need to be decrypted first")
	}

	trailerDict := r.parser.GetTrailer()
	if trailerDict == nil {
		return fmt.Errorf("missing trailer")
	}

	// Catalog.
	root, ok := trailerDict.Get("Root").(*core.PdfObjectReference)
	if !ok {
		return fmt.Errorf("invalid Root (trailer: %s)", trailerDict)
	}
	oc, err := r.parser.LookupByReference(*root)
	if err != nil {
		common.Log.Debug("ERROR: Failed to read root element catalog: %s", err)
		return err
	}
	pcatalog, ok := oc.(*core.PdfIndirectObject)
	if !ok {
		common.Log.Debug("ERROR: Missing catalog: (root %q) (trailer %s)", oc, *trailerDict)
		return errors.New("missing catalog")
	}
	catalog, ok := (*pcatalog).PdfObject.(*core.PdfObjectDictionary)
	if !ok {
		common.Log.Debug("ERROR: Invalid catalog (%s)", pcatalog.PdfObject)
		return errors.New("invalid catalog")
	}
	common.Log.Trace("Catalog: %s", catalog)

	// Pages.
	pagesRef, ok := catalog.Get("Pages").(*core.PdfObjectReference)
	if !ok {
		return errors.New("pages in catalog should be a reference")
	}
	op, err := r.parser.LookupByReference(*pagesRef)
	if err != nil {
		common.Log.Debug("ERROR: Failed to read pages")
		return err
	}
	ppages, ok := op.(*core.PdfIndirectObject)
	if !ok {
		common.Log.Debug("ERROR: Pages object invalid")
		common.Log.Debug("op: %p", ppages)
		return errors.New("pages object invalid")
	}
	pages, ok := ppages.PdfObject.(*core.PdfObjectDictionary)
	if !ok {
		common.Log.Debug("ERROR: Pages object invalid (%s)", ppages)
		return errors.New("pages object invalid")
	}
	pageCount, ok := core.GetInt(pages.Get("Count"))
	if !ok {
		common.Log.Debug("ERROR: Pages count object invalid")
		return errors.New("pages count invalid")
	}
	if _, ok = core.GetName(pages.Get("Type")); !ok {
		common.Log.Debug("Pages dict Type field not set. Setting Type to Pages.")
		pages.Set("Type", core.MakeName("Pages"))
	}

	r.root = root
	r.catalog = catalog
	r.pages = pages
	r.pagesContainer = ppages
	r.pageCount = int(*pageCount)
	r.pageList = []*core.PdfIndirectObject{}

	traversedPageNodes := map[core.PdfObject]struct{}{}
	err = r.buildPageList(ppages, nil, traversedPageNodes)
	if err != nil {
		return err
	}
	common.Log.Trace("---")
	common.Log.Trace("TOC")
	common.Log.Trace("Pages")
	common.Log.Trace("%d: %s", len(r.pageList), r.pageList)

	// Outlines.
	r.outlineTree, err = r.loadOutlines()
	if err != nil {
		common.Log.Debug("ERROR: Failed to build outline tree (%s)", err)
		return err
	}

	// Load interactive forms and fields.
	r.AcroForm, err = r.loadForms()
	if err != nil {
		return err
	}

	return nil
}

func (r *PdfReader) loadOutlines() (*PdfOutlineTreeNode, error) {
	if r.parser.GetCrypter() != nil && !r.parser.IsAuthenticated() {
		return nil, fmt.Errorf("file need to be decrypted first")
	}

	// Has outlines? Otherwise return an empty outlines structure.
	catalog := r.catalog
	outlinesObj := catalog.Get("Outlines")
	if outlinesObj == nil {
		return nil, nil
	}

	common.Log.Trace("-Has outlines")
	// Trace references to the object.
	outlineRootObj := core.ResolveReference(outlinesObj)
	common.Log.Trace("Outline root: %v", outlineRootObj)

	if _, isNull := outlineRootObj.(*core.PdfObjectNull); isNull {
		common.Log.Trace("Outline root is null - no outlines")
		return nil, nil
	}

	outlineRoot, ok := outlineRootObj.(*core.PdfIndirectObject)
	if !ok {
		if _, ok := core.GetDict(outlineRootObj); !ok {
			common.Log.Debug("Invalid outline root - skipping")
			return nil, nil
		}

		common.Log.Debug("Outline root is a dict. Should be an indirect object")
		outlineRoot = core.MakeIndirectObject(outlineRootObj)
	}

	dict, ok := outlineRoot.PdfObject.(*core.PdfObjectDictionary)
	if !ok {
		return nil, errors.New("outline indirect object should contain a dictionary")
	}

	common.Log.Trace("Outline root dict: %v", dict)

	outlineTree, _, err := r.buildOutlineTree(outlineRoot, nil, nil, nil)
	if err != nil {
		return nil, err
	}
	common.Log.Trace("Resulting outline tree: %v", outlineTree)

	return outlineTree, nil
}

// Recursive build outline tree.
// prev PdfObject,
// Input: The indirect object containing an Outlines or Outline item dictionary.
// Parent, Prev are the parent or previous node in the hierarchy.
// The function returns the corresponding tree node and the last node which is used
// for setting the Last pointer of the tree node structures.
func (r *PdfReader) buildOutlineTree(obj core.PdfObject, parent *PdfOutlineTreeNode, prev *PdfOutlineTreeNode, visited map[core.PdfObject]struct{}) (*PdfOutlineTreeNode, *PdfOutlineTreeNode, error) {
	if visited == nil {
		visited = map[core.PdfObject]struct{}{}
	}
	visited[obj] = struct{}{}

	container, isInd := obj.(*core.PdfIndirectObject)
	if !isInd {
		return nil, nil, fmt.Errorf("outline container not an indirect object %T", obj)
	}
	dict, ok := container.PdfObject.(*core.PdfObjectDictionary)
	if !ok {
		return nil, nil, errors.New("not a dictionary object")
	}
	common.Log.Trace("build outline tree: dict: %v (%v) p: %p", dict, container, container)

	if obj := dict.Get("Title"); obj != nil {
		// Outline item has a title. (required)
		outlineItem, err := r.newPdfOutlineItemFromIndirectObject(container)
		if err != nil {
			return nil, nil, err
		}
		outlineItem.Parent = parent
		outlineItem.Prev = prev

		if firstObj := dict.Get("First"); firstObj != nil {
			firstObj = core.ResolveReference(firstObj)
			if !core.IsNullObject(firstObj) {
				first, last, err := r.buildOutlineTree(firstObj, &outlineItem.PdfOutlineTreeNode, nil, visited)
				if err != nil {
					common.Log.Debug("DEBUG: could not build outline item tree: %v. Skipping node children.", err)
				} else {
					outlineItem.First = first
					outlineItem.Last = last
				}
			}
		}

		// Resolve the reference to next
		nextObj := core.ResolveReference(dict.Get("Next"))
		if _, processed := visited[nextObj]; nextObj != nil && nextObj != container && !processed {
			if _, isNull := nextObj.(*core.PdfObjectNull); !isNull {
				next, last, err := r.buildOutlineTree(nextObj, parent, &outlineItem.PdfOutlineTreeNode, visited)
				if err != nil {
					common.Log.Debug("DEBUG: could not build outline tree for Next node: %v. Skipping node.", err)
				} else {
					outlineItem.Next = next
					return &outlineItem.PdfOutlineTreeNode, last, nil
				}
			}
		}

		return &outlineItem.PdfOutlineTreeNode, &outlineItem.PdfOutlineTreeNode, nil
	}

	// Outline dictionary (structure element).
	outline, err := newPdfOutlineFromIndirectObject(container)
	if err != nil {
		return nil, nil, err
	}
	outline.Parent = parent

	if firstObj := dict.Get("First"); firstObj != nil {
		// Has children...
		firstObj = core.ResolveReference(firstObj)
		firstObjDirect := core.TraceToDirectObject(firstObj)
		if _, isNull := firstObjDirect.(*core.PdfObjectNull); !isNull && firstObjDirect != nil {
			first, last, err := r.buildOutlineTree(firstObj, &outline.PdfOutlineTreeNode, nil, visited)
			if err != nil {
				common.Log.Debug("DEBUG: could not build outline tree: %v. Skipping node children.", err)
			} else {
				outline.First = first
				outline.Last = last
			}
		}
	}

	return &outline.PdfOutlineTreeNode, &outline.PdfOutlineTreeNode, nil
}

// GetOutlineTree returns the outline tree.
func (r *PdfReader) GetOutlineTree() *PdfOutlineTreeNode {
	return r.outlineTree
}

// GetOutlinesFlattened returns a flattened list of tree nodes and titles.
// NOTE: for most use cases, it is recommended to use the high-level GetOutlines
// method instead, which also provides information regarding the destination
// of the outline items.
func (r *PdfReader) GetOutlinesFlattened() ([]*PdfOutlineTreeNode, []string, error) {
	var outlineNodeList []*PdfOutlineTreeNode
	var flattenedTitleList []string

	// Recursive flattening function.
	var flattenFunc func(*PdfOutlineTreeNode, *[]*PdfOutlineTreeNode, *[]string, int)
	flattenFunc = func(node *PdfOutlineTreeNode, outlineList *[]*PdfOutlineTreeNode, titleList *[]string, depth int) {
		if node == nil {
			return
		}
		if node.context == nil {
			common.Log.Debug("ERROR: Missing node.context") // Should not happen ever.
			return
		}

		item, isItem := node.context.(*PdfOutlineItem)
		if isItem {
			*outlineList = append(*outlineList, &item.PdfOutlineTreeNode)
			title := strings.Repeat(" ", depth*2) + item.Title.Decoded()
			*titleList = append(*titleList, title)
		}

		if node.First != nil {
			title := strings.Repeat(" ", depth*2) + "+"
			*titleList = append(*titleList, title)
			flattenFunc(node.First, outlineList, titleList, depth+1)
		}

		if isItem && item.Next != nil {
			flattenFunc(item.Next, outlineList, titleList, depth)
		}
	}
	flattenFunc(r.outlineTree, &outlineNodeList, &flattenedTitleList, 0)
	return outlineNodeList, flattenedTitleList, nil
}

// GetOutlines returns a high-level Outline object, based on the outline tree
// of the reader.
func (r *PdfReader) GetOutlines() (*Outline, error) {
	if r == nil {
		return nil, errors.New("cannot create outline from nil reader")
	}

	outlineTree := r.GetOutlineTree()
	if outlineTree == nil {
		return nil, errors.New("the specified reader does not have an outline tree")
	}

	var traverseFunc func(node *PdfOutlineTreeNode, entries *[]*OutlineItem)
	traverseFunc = func(node *PdfOutlineTreeNode, entries *[]*OutlineItem) {
		if node == nil {
			return
		}
		if node.context == nil {
			common.Log.Debug("ERROR: missing outline entry context")
			return
		}

		// Check if node is an outline item.
		var entry *OutlineItem
		if item, ok := node.context.(*PdfOutlineItem); ok {
			// Search for outline destination object.
			destObj := item.Dest
			if (destObj == nil || core.IsNullObject(destObj)) && item.A != nil {
				if actionDict, ok := core.GetDict(item.A); ok {
					destObj, _ = core.GetArray(actionDict.Get("D"))
				}
			}

			// Parse outline destination object.
			var dest OutlineDest
			if destObj != nil && !core.IsNullObject(destObj) {
				if d, err := newOutlineDestFromPdfObject(destObj, r); err == nil {
					dest = *d
				} else {
					common.Log.Debug("WARN: could not parse outline dest (%v): %v", destObj, err)
				}
			}

			entry = NewOutlineItem(item.Title.Decoded(), dest)
			*entries = append(*entries, entry)

			// Traverse next node.
			if item.Next != nil {
				traverseFunc(item.Next, entries)
			}
		}

		// Check if node has children.
		if node.First != nil {
			if entry != nil {
				entries = &entry.Entries
			}

			// Traverse node children.
			traverseFunc(node.First, entries)
		}
	}

	outline := NewOutline()
	traverseFunc(outlineTree, &outline.Entries)
	return outline, nil
}

// loadForms loads the AcroForm.
func (r *PdfReader) loadForms() (*PdfAcroForm, error) {
	if r.parser.GetCrypter() != nil && !r.parser.IsAuthenticated() {
		return nil, fmt.Errorf("file need to be decrypted first")
	}

	// Has forms?
	catalog := r.catalog
	obj := catalog.Get("AcroForm")
	if obj == nil {
		// Nothing to load.
		return nil, nil
	}

	formsContainer, _ := core.GetIndirect(obj)

	obj = core.TraceToDirectObject(obj)
	if core.IsNullObject(obj) {
		common.Log.Trace("Acroform is a null object (empty)\n")
		return nil, nil
	}

	formsDict, ok := core.GetDict(obj)
	if !ok {
		common.Log.Debug("Invalid AcroForm entry %T", obj)
		common.Log.Debug("Does not have forms")
		return nil, fmt.Errorf("invalid acroform entry %T", obj)
	}
	common.Log.Trace("Has Acro forms")
	// Load it.

	// Ensure we have access to everything.
	common.Log.Trace("Traverse the Acroforms structure")
	if !r.isLazy {
		err := r.traverseObjectData(formsDict)
		if err != nil {
			common.Log.Debug("ERROR: Unable to traverse AcroForms (%s)", err)
			return nil, err
		}
	}

	// Create the acro forms object.
	acroForm, err := r.newPdfAcroFormFromDict(formsContainer, formsDict)
	if err != nil {
		return nil, err
	}

	return acroForm, nil
}

func (r *PdfReader) lookupPageByObject(obj core.PdfObject) (*PdfPage, error) {
	// can be indirect, direct, or reference
	// look up the corresponding page
	return nil, errors.New("page not found")
}

// Build the table of contents.
// tree, ex: Pages -> Pages -> Pages -> Page
// Traverse through the whole thing recursively.
func (r *PdfReader) buildPageList(node *core.PdfIndirectObject, parent *core.PdfIndirectObject, traversedPageNodes map[core.PdfObject]struct{}) error {
	if node == nil {
		return nil
	}

	if _, alreadyTraversed := traversedPageNodes[node]; alreadyTraversed {
		common.Log.Debug("Cyclic recursion, skipping (%v)", node.ObjectNumber)
		return nil
	}
	traversedPageNodes[node] = struct{}{}

	nodeDict, ok := node.PdfObject.(*core.PdfObjectDictionary)
	if !ok {
		return errors.New("node not a dictionary")
	}

	objType, ok := (*nodeDict).Get("Type").(*core.PdfObjectName)
	if !ok {
		if nodeDict.Get("Kids") == nil {
			return errors.New("node missing Type (Required)")
		}

		common.Log.Debug("ERROR: node missing Type, but has Kids. Assuming Pages node.")
		objType = core.MakeName("Pages")
		nodeDict.Set("Type", objType)
	}
	common.Log.Trace("buildPageList node type: %s (%+v)", *objType, node)
	if *objType == "Page" {
		p, err := r.newPdfPageFromDict(nodeDict)
		if err != nil {
			return err
		}
		p.setContainer(node)

		if parent != nil {
			// Set the parent (in case missing or incorrect).
			nodeDict.Set("Parent", parent)
		}
		r.pageList = append(r.pageList, node)
		r.PageList = append(r.PageList, p)

		return nil
	}
	if *objType != "Pages" {
		common.Log.Debug("ERROR: Table of content containing non Page/Pages object! (%s)", objType)
		return errors.New("table of content containing non Page/Pages object")
	}

	// A Pages object.  Update the parent.
	if parent != nil {
		nodeDict.Set("Parent", parent)
	}

	// Resolve the object recursively.
	if !r.isLazy {
		err := r.traverseObjectData(node)
		if err != nil {
			return err
		}
	}

	kidsObj, err := r.parser.Resolve(nodeDict.Get("Kids"))
	if err != nil {
		common.Log.Debug("ERROR: Failed loading Kids object")
		return err
	}

	var kids *core.PdfObjectArray
	kids, ok = kidsObj.(*core.PdfObjectArray)
	if !ok {
		kidsIndirect, isIndirect := kidsObj.(*core.PdfIndirectObject)
		if !isIndirect {
			return errors.New("invalid Kids object")
		}
		kids, ok = kidsIndirect.PdfObject.(*core.PdfObjectArray)
		if !ok {
			return errors.New("invalid Kids indirect object")
		}
	}
	common.Log.Trace("Kids: %s", kids)
	for idx, child := range kids.Elements() {
		child, ok := core.GetIndirect(child)
		if !ok {
			common.Log.Debug("ERROR: Page not indirect object - (%s)", child)
			return errors.New("page not indirect object")
		}
		kids.Set(idx, child)
		err = r.buildPageList(child, node, traversedPageNodes)
		if err != nil {
			return err
		}
	}

	return nil
}

// GetNumPages returns the number of pages in the document.
func (r *PdfReader) GetNumPages() (int, error) {
	if r.parser.GetCrypter() != nil && !r.parser.IsAuthenticated() {
		return 0, fmt.Errorf("file need to be decrypted first")
	}
	return len(r.pageList), nil
}

// Resolves a reference, returning the object and indicates whether or not
// it was cached.
func (r *PdfReader) resolveReference(ref *core.PdfObjectReference) (core.PdfObject, bool, error) {
	cachedObj, isCached := r.parser.ObjCache[int(ref.ObjectNumber)]
	if !isCached {
		common.Log.Trace("Reader Lookup ref: %s", ref)
		obj, err := r.parser.LookupByReference(*ref)
		if err != nil {
			return nil, false, err
		}
		r.parser.ObjCache[int(ref.ObjectNumber)] = obj
		return obj, false, nil
	}
	return cachedObj, true, nil
}

/*
 * Recursively traverse through the page object data and look up
 * references to indirect objects.
 *
 * GH: Are we fully protected against circular references? (Add tests).
 */
func (r *PdfReader) traverseObjectData(o core.PdfObject) error {
	return core.ResolveReferencesDeep(o, r.traversed)
}

// PageFromIndirectObject returns the PdfPage and page number for a given indirect object.
func (r *PdfReader) PageFromIndirectObject(ind *core.PdfIndirectObject) (*PdfPage, int, error) {
	if len(r.PageList) != len(r.pageList) {
		return nil, 0, errors.New("page list invalid")
	}

	for i, pageind := range r.pageList {
		if pageind == ind {
			return r.PageList[i], i + 1, nil
		}
	}
	return nil, 0, errors.New("page not found")
}

// GetPage returns the PdfPage model for the specified page number.
func (r *PdfReader) GetPage(pageNumber int) (*PdfPage, error) {
	if r.parser.GetCrypter() != nil && !r.parser.IsAuthenticated() {
		return nil, fmt.Errorf("file needs to be decrypted first")
	}
	if len(r.pageList) < pageNumber {
		return nil, errors.New("invalid page number (page count too short)")
	}
	idx := pageNumber - 1
	if idx < 0 {
		return nil, fmt.Errorf("page numbering must start at 1")
	}
	page := r.PageList[idx]
	return page, nil
}

// GetOCProperties returns the optional content properties PdfObject.
func (r *PdfReader) GetOCProperties() (core.PdfObject, error) {
	dict := r.catalog
	obj := dict.Get("OCProperties")
	obj = core.ResolveReference(obj)

	// Resolve all references...
	// Should be pretty safe. Should not be referencing to pages or
	// any large structures.  Local structures and references
	// to OC Groups.
	if !r.isLazy {
		err := r.traverseObjectData(obj)
		if err != nil {
			return nil, err
		}
	}

	return obj, nil
}

// GetNamedDestinations returns the Names entry in the PDF catalog.
// See section 12.3.2.3 "Named Destinations" (p. 367 PDF32000_2008).
func (r *PdfReader) GetNamedDestinations() (core.PdfObject, error) {
	obj := core.ResolveReference(r.catalog.Get("Names"))
	if obj == nil {
		return nil, nil
	}

	// Resolve references.
	if !r.isLazy {
		err := r.traverseObjectData(obj)
		if err != nil {
			return nil, err
		}
	}

	return obj, nil
}

// Inspect inspects the object types, subtypes and content in the PDF file returning a map of
// object type to number of instances of each.
func (r *PdfReader) Inspect() (map[string]int, error) {
	return r.parser.Inspect()
}

// GetObjectNums returns the object numbers of the PDF objects in the file
// Numbered objects are either indirect objects or stream objects.
// e.g. objNums := pdfReader.GetObjectNums()
// The underlying objects can then be accessed with
// pdfReader.GetIndirectObjectByNumber(objNums[0]) for the first available object.
func (r *PdfReader) GetObjectNums() []int {
	return r.parser.GetObjectNums()
}

// GetIndirectObjectByNumber retrieves and returns a specific PdfObject by object number.
func (r *PdfReader) GetIndirectObjectByNumber(number int) (core.PdfObject, error) {
	obj, err := r.parser.LookupByNumber(number)
	return obj, err
}

// GetTrailer returns the PDF's trailer dictionary.
func (r *PdfReader) GetTrailer() (*core.PdfObjectDictionary, error) {
	trailerDict := r.parser.GetTrailer()
	if trailerDict == nil {
		return nil, errors.New("trailer missing")
	}

	return trailerDict, nil
}
